/*
 *  Copyright 2005 Blandware (http://www.blandware.com)
 *
 *  Licensed under the Apache License, Version 2.0 (the "License");
 *  you may not use this file except in compliance with the License.
 *  You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 *  Unless required by applicable law or agreed to in writing, software
 *  distributed under the License is distributed on an "AS IS" BASIS,
 *  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 *  See the License for the specific language governing permissions and
 *  limitations under the License.
 */
package com.blandware.atleap.service.core.util;

import com.blandware.atleap.common.Constants;
import com.blandware.atleap.model.core.statistics.*;
import org.apache.commons.digester.Digester;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.apache.commons.validator.GenericValidator;
import org.apache.oro.text.regex.*;
import org.xml.sax.SAXException;

import java.io.IOException;
import java.io.InputStream;
import java.io.UnsupportedEncodingException;
import java.util.*;
import java.net.URLDecoder;

/**
 * <p>Contains utils to work with user agents and search engines.</p>
 * <p><a href="VisitorSoftwareUtil.java.html"><i>View source</i></a></p>
 *
 * @author Roman Puchkovskiy <a href="mailto:roman.puchkovskiy@blandware.com">
 *         &lt;roman.puchkovskiy@blandware.com&gt;</a>
 * @version $Revision: 1.6 $ $Date: 2007/12/20 13:41:55 $
 */
public class VisitorSoftwareUtil {

	/**
	 * Commons Logging instance.
	 */
	protected transient final Log log = LogFactory.getLog(VisitorSoftwareUtil.class);

    /**
     * File that contains XML defining user agents
     */
	private static final String VISITOR_SOFTWARE_FILE = "visitor-software.xml";

    /**
     * Browser descriptions (as map and list: it as assumed that they are
     * synchronized)
     */
	private Map browserDescriptionsMap = new HashMap();
    private List browserDescriptionsList = new ArrayList();

    /**
     * OS descriptions (as map and list: it as assumed that they are
     * synchronized)
     */
    private Map osDescriptionsMap = new HashMap();
    private List osDescriptionsList = new ArrayList();

    /**
     * Robots descriptions (as map and list: it as assumed that they are
     * synchronized)
     */
    private Map robotDescriptionsMap = new HashMap();
    private List robotDescriptionsList = new ArrayList();

    /**
     * Search engine descriptions (as map and list: it as assumed that they are
     * synchronized)
     */
    private Map searchEngineDescriptionsMap = new HashMap();
    private List searchEngineDescriptionsList = new ArrayList();

    /**
     * Initializes instance.
     */
    public void initialize() {

        if (log.isDebugEnabled()) {
            log.debug("Initializing VisitorSoftwareUtil...");
        }

        ClassLoader classLoader = Thread.currentThread().getContextClassLoader();
        if (classLoader == null) {
            classLoader = getClass().getClassLoader();
        }

        InputStream is = classLoader.getResourceAsStream(VISITOR_SOFTWARE_FILE);
		if ( is == null ) {
			if ( log.isErrorEnabled() ) {
				log.error("Could not locate xml file with user agents: "
                        + VISITOR_SOFTWARE_FILE);
			}
		}
		try {
			Digester digester = new Digester();
			digester.push(this);
			digester.setNamespaceAware(false);
			digester.setValidating(false);

			digester.addObjectCreate("statistics/userAgents/browser",
                    BrowserDescription.class);
			digester.addSetProperties("statistics/userAgents/browser");
			digester.addSetNext("statistics/userAgents/browser",
                                "addBrowserDescription",
                                BrowserDescription.class.getName());

			digester.addObjectCreate("statistics/userAgents/os",
                                     OSDescription.class);
			digester.addSetProperties("statistics/userAgents/os");
			digester.addSetNext("statistics/userAgents/os",
                                "addOSDescription",
                                OSDescription.class.getName());

			digester.addObjectCreate("statistics/userAgents/robot",
                                     RobotDescription.class);
			digester.addSetProperties("statistics/userAgents/robot");
			digester.addSetNext("statistics/userAgents/robot",
                                "addRobotDescription",
                                RobotDescription.class.getName());

			digester.addObjectCreate("statistics/searchEngines/searchEngine",
                                     SearchEngineDescription.class);
			digester.addSetProperties("statistics/searchEngines/searchEngine");
			digester.addSetNext("statistics/searchEngines/searchEngine",
                                "addSearchEngineDescription",
                                SearchEngineDescription.class.getName());
			digester.parse(is);

		} catch ( IOException e ) {
			if ( log.isErrorEnabled() ) {
				log.error("Could not load xml file with user agents: "
                        + VISITOR_SOFTWARE_FILE, e);
			}
		} catch ( SAXException e ) {
			if ( log.isErrorEnabled() ) {
				log.error("Could not parse xml file with user agents: "
                        + VISITOR_SOFTWARE_FILE, e);
			}
		}
    }

    /**
     * Adds a browser description.
     *
     * @param description description to add
     */
    public void addBrowserDescription(BrowserDescription description) {
        Integer id = description.getId();
        if (browserDescriptionsMap.get(id) != null) {
            if (log.isWarnEnabled()) {
                log.warn("Browser description for ID=" + id + " is not unique; skipping");
            }
        } else {
            if (Constants.UNKNOWN_SOFTWARE_ID.equals(id)) {
                if (log.isWarnEnabled()) {
                    log.warn("Browser description for ID="
                            + id + " is skipped, because this ID is reserved");
                }
            } else {
                browserDescriptionsMap.put(id, description);
                browserDescriptionsList.add(description);
            }
        }
    }

    /**
     * Adds an OS description.
     *
     * @param description description to add
     */
    public void addOSDescription(OSDescription description) {
        Integer id = description.getId();
        if (osDescriptionsMap.get(id) != null) {
            if (log.isWarnEnabled()) {
                log.warn("OS description for ID=" + id + " is not unique; skipping");
            }
        } else {
            if (Constants.UNKNOWN_SOFTWARE_ID.equals(id)) {
                if (log.isWarnEnabled()) {
                    log.warn("OS description for ID="
                            + id + " is skipped, because this ID is reserved");
                }
            } else {
                osDescriptionsMap.put(id, description);
                osDescriptionsList.add(description);
            }
        }
    }

    /**
     * Adds a robot description.
     *
     * @param description description to add
     */
    public void addRobotDescription(RobotDescription description) {
        Integer id = description.getId();
        if (robotDescriptionsMap.get(id) != null) {
            if (log.isWarnEnabled()) {
                log.warn("Robot description for ID=" + id + " is not unique; skipping");
            }
        } else {
            if (Constants.UNKNOWN_SOFTWARE_ID.equals(id)) {
                if (log.isWarnEnabled()) {
                    log.warn("Robot description for ID="
                            + id + " is skipped, because this ID is reserved");
                }
            } else {
                robotDescriptionsMap.put(id, description);
                robotDescriptionsList.add(description);
            }
        }
    }

    /**
     * Adds a search engine description.
     *
     * @param description description to add
     */
    public void addSearchEngineDescription(SearchEngineDescription description) {
        Integer id = description.getId();
        if (searchEngineDescriptionsMap.get(id) != null) {
            if (log.isWarnEnabled()) {
                log.warn("Search engine description for ID=" + id + " is not unique; skipping");
            }
        } else {
            if (Constants.UNKNOWN_SOFTWARE_ID.equals(id)) {
                if (log.isWarnEnabled()) {
                    log.warn("Search engine description for ID="
                            + id + " is skipped, because this ID is reserved");
                }
            } else {
                searchEngineDescriptionsMap.put(id, description);
                searchEngineDescriptionsList.add(description);
            }
        }
    }

    /**
     * Returns browser ID detected by user agent string.
     *
     * @param userAgent user agent string by which to detect browser
     * @return browser ID (-1 for unknown)
     */
    public Integer detectBrowserIdByUserAgentString(String userAgent) {
        return detectUserAgentIdByUserAgentString(userAgent,
                                                  browserDescriptionsList);
    }

    /**
     * Returns OS ID detected by user agent string.
     *
     * @param userAgent user agent string by which to detect OS
     * @return OS ID (-1 for unknown)
     */
    public Integer detectOsIdByUserAgentString(String userAgent) {
        return detectUserAgentIdByUserAgentString(userAgent,
                                                  osDescriptionsList);
    }

    /**
     * Returns robot ID detected by user agent string.
     *
     * @param userAgent user agent string by which to detect robot
     * @return robot ID (-1 for unknown)
     */
    public Integer detectRobotIdByUserAgentString(String userAgent) {
        return detectUserAgentIdByUserAgentString(userAgent,
                                                  robotDescriptionsList);
    }

    /**
     * Returns user agent ID detected by user agent string and descriptions.
     *
     * @param userAgent user agent string by which to detect
     * @param descriptions list of descriptions which to use when detecting;
     * order of descriptions inside list is significant
     * @return user agent ID (-1 for unknown)
     */
    protected Integer detectUserAgentIdByUserAgentString(String userAgent,
                                                         List descriptions) {
        PatternCompiler compiler = new Perl5Compiler();
        PatternMatcher matcher = new Perl5Matcher();
        Integer result = Constants.UNKNOWN_SOFTWARE_ID;
        for (Iterator i = descriptions.iterator(); i.hasNext(); ) {
            ClientSideSoftwareDescription description = (ClientSideSoftwareDescription) i.next();
            Integer id = description.getId();
            String regex = description.getPattern();
            String antiRegex = description.getAntiPattern();

            Pattern pattern;
            Pattern antiPattern;
            try {
                pattern = compiler.compile(regex);
            } catch (MalformedPatternException e) {
                if (log.isErrorEnabled()) {
                    log.error("Bad regexp: " + regex, e);
                }
                continue;
            }
            if (matcher.contains(userAgent, pattern)) {
                if (antiRegex.length() == 0) {
                    result = id;
                    break;
                } else {
                    try {
                        antiPattern = compiler.compile(antiRegex);
                    } catch (MalformedPatternException e) {
                        if (log.isErrorEnabled()) {
                            log.error("Bad regexp: " + antiRegex, e);
                        }
                        continue;
                    }
                    if (!matcher.contains(userAgent, antiPattern)) {
                        result = id;
                        break;
                    }
                }
            }
        }

        return result;
    }

    /**
     * Obtains a user agent object filling its fields with values detected on
     * User-Agent string.
     *
     * @param userAgentString User-Agent string
     * @return filled UserAgent instance
     */
    public UserAgent detectUserAgent(String userAgentString) {
        UserAgent result;
        boolean userAgentIsNull = userAgentString == null;

        Integer browserId = Constants.UNKNOWN_SOFTWARE_ID;
        Integer osId = Constants.UNKNOWN_SOFTWARE_ID;

        if (!userAgentIsNull) {
            browserId = detectBrowserIdByUserAgentString(userAgentString);
            osId = detectOsIdByUserAgentString(userAgentString);
        }
        BrowserDescription browserDescription = getBrowserDescription(browserId);
        OSDescription osDescription = getOSDescription(osId);
        if (!Constants.UNKNOWN_SOFTWARE_ID.equals(browserId)) {
            // that's a browser
            result = new UserAgent(browserDescription, osDescription);
        } else {
            // that's probably a robot
            Integer robotId = Constants.UNKNOWN_SOFTWARE_ID;
            if (!userAgentIsNull) {
                robotId = detectRobotIdByUserAgentString(userAgentString);
            }
            if (!Constants.UNKNOWN_SOFTWARE_ID.equals(robotId)) {
                // yes, that's a robot
                RobotDescription robotDescription = getRobotDescription(robotId);
                result = new UserAgent(robotDescription);
            } else {
                // no, so we don't know what is it, and assume that it's an
                // unknown browser
                result = new UserAgent(browserDescription, osDescription);
            }
        }
        return result;
    }

    /**
     * Detects a search engine ID by referrer domain name.
     *
     * @param referrerDomain domain by which to detect
     * @return search engine ID (-1 for unknown)
     */
    public Integer detectSearchEngineId(String referrerDomain) {
        Integer result = Constants.UNKNOWN_SOFTWARE_ID;
        if (!GenericValidator.isBlankOrNull(referrerDomain)) {
            for (Iterator i = searchEngineDescriptionsList.iterator(); i.hasNext();) {
                SearchEngineDescription description = (SearchEngineDescription) i.next();
                Integer id = description.getId();
                String pattern = description.getPattern();
                if (referrerDomain.indexOf(pattern) != -1) {
                    result = id;
                    break;
                }
            }
        }
        return result;
    }

    /**
     * Returns search query found in query parameters for a given search engine.
     *
     * @param queryParameters map that contains all query parameters; all values
     * are URL-encoded
     * @param searchEngineId ID of search engine
     * @return search phrase
     */
    public String extractSearchPhrase(Map queryParameters,
                                      Integer searchEngineId) {
        String result = null;

        SearchEngineDescription description = getSearchEngineDescription(searchEngineId);
        String var = description.getVar();
        String[] values = (String[]) queryParameters.get(var);
        if (values != null && values.length > 0) {
            result = values[0];
            String encoding = description.getQueryEncoding();
            if (GenericValidator.isBlankOrNull(encoding)) {
                encoding = Constants.DEFAULT_ENCODING;
            }
            try {
                result = URLDecoder.decode(result, encoding);
            } catch (UnsupportedEncodingException e) {
                // do nothing
            }
        } else {
            String subvar = description.getSubvar();
            if (!GenericValidator.isBlankOrNull(subvar)) {
                String[] subvarValues = (String[]) queryParameters.get(subvar);
                if (subvarValues != null && subvarValues.length > 0
                        && subvarValues[0] != null) {
                    String queryString = subvarValues[0];
                    try {
                        queryString = URLDecoder.decode(queryString, Constants.DEFAULT_ENCODING);
                    } catch (UnsupportedEncodingException e) {
                        // do nothing
                    }
                    String encoding = description.getSubvarEncoding();
                    if (GenericValidator.isBlankOrNull(encoding)) {
                        encoding = Constants.DEFAULT_ENCODING;
                    }

                    for ( int k = 0; k < queryString.length(); ) {
                        int ampPos = queryString.indexOf('&', k);
                        if ( ampPos == -1 ) {
                            ampPos = queryString.length();
                        }
                        String parameter = queryString.substring(k, ampPos);
                        int equalsSignPos = parameter.indexOf('=');
                        if (equalsSignPos != -1) {
                            String key = parameter.substring(0, equalsSignPos);
                            String value = parameter.substring(equalsSignPos + 1);
                            try {
                                key = URLDecoder.decode(key, encoding);
                                value = URLDecoder.decode(value, encoding);
                                if (key != null && key.equals(var)) {
                                    result = value;
                                    break;
                                }
                            } catch ( UnsupportedEncodingException e ) {
                                // do nothing
                            }
                        }
                        k = ampPos + 1;
                    }
                }
            }
        }

        return result;
    }

    /**
     * Returns browser description by its ID. If no description with such ID
     * found, <code>null</code> is returned.
     *
     * @param browserId ID of browser which description to obtain
     * @return browser description
     */
    public BrowserDescription getBrowserDescription(Integer browserId) {
        if (browserId == null) {
            return null;
        }
        return (BrowserDescription) browserDescriptionsMap.get(browserId);
    }

    /**
     * Returns OS description by its ID. If no description with such ID
     * found, <code>null</code> is returned.
     *
     * @param osId ID of OS which description to obtain
     * @return OS description
     */
    public OSDescription getOSDescription(Integer osId) {
        if (osId == null) {
            return null;
        }
        return (OSDescription) osDescriptionsMap.get(osId);
    }

    /**
     * Returns robot description by its ID. If no description with such ID
     * found, <code>null</code> is returned.
     *
     * @param robotId ID of robot which description to obtain
     * @return robot description
     */
    public RobotDescription getRobotDescription(Integer robotId) {
        if (robotId == null) {
            return null;
        }
        return (RobotDescription) robotDescriptionsMap.get(robotId);
    }

    /**
     * Returns search engine description by its ID. If no description with such
     * ID found, <code>null</code> is returned.
     *
     * @param searchEngineId ID of search engine which description to obtain
     * @return search engine description
     */
    public SearchEngineDescription getSearchEngineDescription(Integer searchEngineId) {
        if (searchEngineId == null) {
            return null;
        }
        return (SearchEngineDescription) searchEngineDescriptionsMap.get(searchEngineId);
    }
}
